#Region "Imports"
Imports System.IO
Imports System.Collections.Generic
#End Region
''' <summary>
''' Main class for reading and modifying a PDF file.
''' It is based on the PDF standard version 1.7.
''' </summary>
''' <remarks>Encrypted PDF files are not supported by this library</remarks>
Public Class ClsPDF

#Region "Private members"
    Private _pdfFile As FileInfo
    Private _pdfReader As ClsPdfReader  'This is the main PDF parser
    Private _version As String          'Version string of the PDF file
    Private _linearized As Boolean      'True if the structure of the pdf file is optimized for rapid viewing
    Private _xrefTable As New Hashtable 'This is the whole PDF cross-reference table for random access to indirect objects in the file  
    Private _trailerDictionnary As clsPdfDictionnary    'The PDF file trailer dictionnary
    Private _catalogDictionnary As clsPdfDictionnary    'The main catalog dictionnary
    Private _viewPreferencesDictionnary As clsPDFViewerPreferences 'The viewer preferences
    Private _documentInformation As ClsPdfDocumentInformation   'The PDF file information dictionnary
    Private _encryptionDictionnary As clsPdfEncryption 'If the PDF is encrypted, this dictionary contains the details
    Private _pageTree As clsPDFPageTree 'All the pages
    Private _pageCount As Long  'Total number of pages in the PDF file
    Private _nextFreeObjectNumber As Long   'The next number to use when adding new objects to the pdf
    Private _lastXrefTable As Long  'Position of the last cross-reference table in the PDF
    Private _pdfWriter As ClsPdfWriter 'The pdf writer
    Private _images As New List(Of clsPDFImage)    'Images added to the PDF
    Private _objectStreamsCache As New Dictionary(Of Long, clsPDFObjectStream)    'Cache for decoded object streams
#End Region
#Region "Constructors"
    ''' <summary>
    ''' Open a PDF file
    ''' </summary>
    ''' <param name="fichierPath">File path</param>
    ''' <remarks></remarks>
    Public Sub New(ByVal fichierPath As String)
        Dim obj As clsPdfBaseObject
        Dim newTrailer As clsPdfDictionnary
        Dim position As Integer
        Dim crossReferenceRead As Boolean
        Dim word As String

        Try
            _pdfFile = New FileInfo(fichierPath)
            _pdfReader = New ClsPdfReader(fichierPath)

            'Before going any further, we check if the file is a PDF document
            If Not _isFilePDF() Then
                Throw New Exception("The file is not a PDF")
            End If

            _trailerDictionnary = New clsPdfDictionnary
            crossReferenceRead = False

            _linearized = _isPDFLinearized()

            If _linearized Then
                'The PDF is optimized for rapid viewing

                'We read the offset to the main cross reference table
                position = _pdfReader.Reader.ReaderPosition
                obj = _pdfReader.Reader.GetObject
                Select Case obj.Type
                    Case PdfObjectTypes.IndirectObject
                        'The main cross-reference is a cross-reference streams
                        _pdfReader.LastXrefTablePosition = obj.Position
                        obj.ParseChildObjects()
                        If obj.ChildsCount = 2 Then
                            If obj.child(0).Type = PdfObjectTypes.DictionaryObject And obj.child(1).Type = PdfObjectTypes.StreamObject Then
                                'We have a cross-reference stream
                                crossReferenceRead = _pdfReader.ReadCrossReferenceTableStream(obj, _xrefTable)
                            End If
                        End If

                        If Not crossReferenceRead Then
                            Throw New Exception("Error while reading the main cross-reference stream in a linearized PDF")
                        End If

                        newTrailer = _pdfReader.TrailerFromStream
                        _addToTrailer(newTrailer)

                    Case Else
                        'The main cross-reference is a standard cross-reference 

                        _pdfReader.Reader.ReaderPosition = position
                        word = _pdfReader.Reader.GetWord()
                        While word <> PDF_XREF
                            word = _pdfReader.Reader.GetWord()
                        End While

                        _lastXrefTable = _pdfReader.Reader.ReaderPosition - (PDF_XREF.Length + 1)

                        While Not _pdfReader.Reader.Eof
                            _pdfReader.ReadCrossReferenceTable(_pdfReader.Reader.ReaderPosition, _xrefTable)
                            word = _pdfReader.Reader.GetWord()
                            If word = PDF_TRAILER Then
                                Exit While
                            Else
                                _pdfReader.Reader.ReaderPosition = position
                            End If
                        End While
                        crossReferenceRead = True

                        newTrailer = _pdfReader.ReadTrailer(_pdfReader.Reader.ReaderPosition).GetPdfDictionnary
                        _addToTrailer(newTrailer)
                End Select
            Else
                'The PDF is not linearized
                'We read the cross-reference table at the end of the PDF file
                If Not _pdfReader.ReadBottomCrossReferenceTable(_xrefTable) Then
                    Throw New Exception("Error while reading the main cross-reference table")
                End If

                newTrailer = _pdfReader.TrailerFromStream
                If Not newTrailer Is Nothing Then
                    _addToTrailer(newTrailer)
                Else
                    newTrailer = _pdfReader.ReadTrailer(_pdfReader.Reader.ReaderPosition).GetPdfDictionnary
                    _addToTrailer(newTrailer)
                    _lastXrefTable = _pdfReader.LastXrefTablePosition
                End If
            End If

            'A PDF file can contain multiple cross-reference tables linked together,
            'we look in the trailer dictionnary to read these cross-reference tables.
            obj = _trailerDictionnary.Item(PDF_PREV)
            While Not obj Is Nothing
                'We have another cross-reference table to read
                _pdfReader.Reader.ReaderPosition = obj.GetLong
                _pdfReader.ReadCrossReferenceTable(_xrefTable)
                newTrailer = _pdfReader.ReadTrailer(_pdfReader.Reader.ReaderPosition).GetPdfDictionnary
                obj = newTrailer.Item(PDF_PREV)
                _addToTrailer(newTrailer)
            End While


            'We get the next pdf free object number that will be attributed to objects added to the pdf file
            _nextFreeObjectNumber = _getNextFreeObject()

            _readEncryptionDictionnary()
            _readCatalog()
            _readViewerPreferences()

            If Not IsEncrypt Then
                'Encryption is not supported by pdflib
                _readDocumentInformation()
                _readAllPageTree(_catalogDictionnary.Item(PDF_PAGES))
            End If
        Catch ex As Exception
            Throw New Exception(ex.Message)
        End Try

    End Sub
#End Region
#Region "Public Properties"
    ''' <summary>
    ''' Gives access to the PDF FileInfo object
    ''' </summary>
    ''' <value></value>
    ''' <returns>FileInfo object</returns>
    ''' <remarks></remarks>
    Public ReadOnly Property FileInfo() As FileInfo
        Get
            FileInfo = _pdfFile
        End Get
    End Property
    ''' <summary>
    ''' Get a specific page number
    ''' </summary>
    ''' <param name="pageNumber">The page number</param>
    ''' <returns>Page object or nothing if pageNumber is outside the pdf's pages range</returns>
    ''' <remarks></remarks>
    Public ReadOnly Property GetPage(ByVal pageNumber As Integer) As clsPDFPage
        Get
            GetPage = _pageTree.GetPageInformation(pageNumber)
        End Get
    End Property
    ''' <summary>
    ''' Provides access to the PDF document information properties
    ''' </summary>
    ''' <returns>Document information object</returns>
    ''' <remarks></remarks>
    Public ReadOnly Property DocumentInformation() As ClsPdfDocumentInformation
        Get
            DocumentInformation = _documentInformation
        End Get
    End Property
    ''' <summary>
    ''' Provides access to the PDF viewer preferences
    ''' </summary>
    ''' <value></value>
    ''' <returns></returns>
    ''' <remarks></remarks>
    Public ReadOnly Property ViewerPreferences As clsPDFViewerPreferences
        Get
            ViewerPreferences = _viewPreferencesDictionnary
        End Get
    End Property
    ''' <summary>
    ''' Provides access to the PDF encryption information
    ''' </summary>
    ''' <returns>PDF encryption object</returns>
    ''' <remarks>Valid only when the PDF is encrypted</remarks>
    Public ReadOnly Property EncryptionInformation() As clsPdfEncryption
        Get
            EncryptionInformation = _encryptionDictionnary
        End Get
    End Property
    ''' <summary>
    ''' Gets a value indicating if the PDF file is encrypted (password protected)
    ''' </summary>
    ''' <returns>True if the file is encrypted, false if it is not.</returns>
    ''' <remarks></remarks>
    Public ReadOnly Property IsEncrypt() As Boolean
        Get
            IsEncrypt = _trailerDictionnary.ContainsKey(PDF_ENCRYPT)
        End Get
    End Property
    ''' <summary>
    ''' Get the version string of the PDF file
    ''' </summary>
    ''' <value></value>
    ''' <returns>PDF version string</returns>
    ''' <remarks></remarks>
    Public ReadOnly Property Version() As String
        Get
            Version = _version
        End Get
    End Property
    ''' <summary>
    ''' Gets the value indicating if the structure of the pdf file is optimized for rapid viewing.
    ''' </summary>
    ''' <value></value>
    ''' <returns>True if the PDF is linearized, False if it is a standard PDF structure</returns>
    ''' <remarks></remarks>
    Public ReadOnly Property IsLinearized() As Boolean
        Get
            IsLinearized = _linearized
        End Get
    End Property
    ''' <summary>
    ''' Get the total number of pages in the PDF
    ''' </summary>
    ''' <value></value>
    ''' <returns>The total number of pages</returns>
    ''' <remarks></remarks>
    Public ReadOnly Property PageCount() As Long
        Get
            PageCount = _pageCount
        End Get
    End Property
#End Region
#Region "Public Methods"
    ''' <summary>
    ''' Create a new viewer preferences for the PDF
    ''' </summary>
    ''' <returns></returns>
    ''' <remarks></remarks>
    Public Function CreateViewerPreferences() As clsPDFViewerPreferences
        _viewPreferencesDictionnary = New clsPDFViewerPreferences(_catalogDictionnary)
        CreateViewerPreferences = _viewPreferencesDictionnary
    End Function
    ''' <summary>
    ''' Get all the pages in the PDF
    ''' </summary>
    ''' <returns>A list of all the pages in the PDF</returns>
    ''' <remarks></remarks>
    Public Function GetPages() As List(Of clsPDFPage)
        Dim i As Integer

        GetPages = New List(Of clsPDFPage)
        For i = 1 To CInt(PageCount)
            GetPages.Add(GetPage(i))
        Next
    End Function
    ''' <summary>
    ''' Get all images contained in the PDF
    ''' </summary>
    ''' <returns>A list of all images</returns>
    ''' <remarks></remarks>
    Public Function GetImages() As List(Of clsPDFImage)
        Dim image As clsPDFImage
        Dim images As New Dictionary(Of String, clsPDFImage)
        _pageTree.GetImages(images)
        GetImages = New List(Of clsPDFImage)
        For Each image In images.Values
            GetImages.Add(image)
        Next
    End Function
    ''' <summary>
    ''' Close the PDF file
    ''' </summary>
    ''' <remarks>Close don't call the save method automatically if there was any modifications applied to the PDF</remarks>
    Public Sub Close()
        _pdfReader = Nothing
    End Sub
    ''' <summary>
    ''' Save changes to another location
    ''' </summary>
    ''' <param name="filePath">Complete file path were to save the modified PDF</param>
    ''' <remarks>Changes to the PDF file are saved in following the incremental updated convention; 
    ''' PDF will lost it's linearized feature if it was.
    ''' Trying to save changes to an encrypted (password protected) PDF file will raise an error.</remarks>
    Public Sub Save(ByVal filePath As String)
        Try
            _writePdfChanges()
            _copyTo(filePath)
        Catch ex As Exception
            Throw New Exception("Error while saving the PDF file :" + ex.Message)
        End Try

    End Sub
    ''' <summary>
    ''' Save changes to the current PDF file
    ''' </summary>
    ''' <remarks>Changes to the PDF file are saved in following the incremental updated convention; 
    ''' PDF will lost it's linearized feature if it was.
    ''' Trying to save changes to an encrypted (password protected) PDF file will raise an error.</remarks>
    Public Sub Save()
        Save(_pdfFile.FullName)
    End Sub
#End Region
#Region "Friend Properties"
    ''' <summary>
    ''' Gives access to the PDF's images list
    ''' </summary>
    ''' <value></value>
    ''' <returns></returns>
    ''' <remarks></remarks>
    Friend ReadOnly Property Images() As List(Of clsPDFImage)
        Get
            Images = _images
        End Get
    End Property
#End Region
#Region "Friend Methods"
    ''' <summary>
    ''' Get an image added to the PDF
    ''' </summary>
    ''' <param name="imageName"></param>
    ''' <returns>An image object</returns>
    ''' <remarks></remarks>
    Friend Function GetImage(ByVal imageName As String) As clsPDFImage
        Dim image As clsPDFImage
        GetImage = Nothing
        For Each image In _images
            If image.Name = imageName Then
                GetImage = image
                Exit For
            End If
        Next
    End Function
    ''' <summary>
    ''' Get a pdf object from a an indirect reference object (a kind of pointer to a pdf object)
    ''' </summary>
    ''' <param name="indirectObject"></param>
    ''' <returns>A pdf object</returns>
    ''' <remarks></remarks>
    Friend Function GetPdfObjectFromIndirectReferenceObject(ByVal indirectObject As clsPdfBaseObject) As clsPdfBaseObject
        Dim xref As ClsPdfXref

        GetPdfObjectFromIndirectReferenceObject = Nothing
        If indirectObject.Type = PdfObjectTypes.IndirectReferenceObject Then

            xref = _getPdfObjectXref(indirectObject)
            If xref.Compressed Then
                'The object is stored in an object stream
                GetPdfObjectFromIndirectReferenceObject = _getPdfObjectFromObjectStream(xref)
            Else
                _pdfReader.Reader.ReaderPosition = xref.Offset
                GetPdfObjectFromIndirectReferenceObject = _pdfReader.Reader.GetObject

                If GetPdfObjectFromIndirectReferenceObject.Type = PdfObjectTypes.UnknownObject Then
                    'Auto-correction : The offset in the PDF is wrong, we read until we get a valid object
                    While GetPdfObjectFromIndirectReferenceObject.Type = PdfObjectTypes.UnknownObject
                        GetPdfObjectFromIndirectReferenceObject = _pdfReader.Reader.GetObject
                    End While
                    If GetPdfObjectFromIndirectReferenceObject.Type <> PdfObjectTypes.IndirectObject Or GetPdfObjectFromIndirectReferenceObject.ObjectNumber <> xref.ObjectNumber Then
                        Throw New Exception("GetPdfObjectFromIndirectReferenceObject error, probably bad offset in PDF file")
                    End If
                End If
            End If
        End If
    End Function
#End Region
#Region "Private Methods"
    Private Function _getPdfObjectFromObjectStream(xref As ClsPdfXref) As clsPdfBaseObject
        Dim xrefCompressed As ClsPdfXref
        Dim objStream As clsPDFObjectStream
        Dim obj As clsPdfBaseObject

        _getPdfObjectFromObjectStream = Nothing
        If _objectStreamsCache.ContainsKey(xref.ObjectStreamNumber) Then
            'The object stream is in the cache, no need to read it again
            objStream = _objectStreamsCache.Item(xref.ObjectStreamNumber)
        Else
            xrefCompressed = _getPdfObjectXref(xref.ObjectStreamNumber)
            _pdfReader.Reader.ReaderPosition = xrefCompressed.Offset
            obj = _pdfReader.Reader.GetObject
            objStream = New clsPDFObjectStream(obj)
        End If
        _getPdfObjectFromObjectStream = objStream.GetObject(xref.ObjectNumber)
    End Function
    ''' <summary>
    ''' Write all the changes in the form of incremental updates
    ''' </summary>
    ''' <remarks></remarks>
    Private Sub _writePdfChanges()
        Dim objectNumber As Long
        Dim image As clsPDFImage
        Dim page As clsPDFPage
        Dim pagesModified As List(Of clsPDFPage)
        Dim obj As clsPdfBaseObject

        If IsEncrypt Then
            Throw New Exception("The PDF is encrypted, changes are not supported by pdflib")
        End If

        _pdfWriter = New ClsPdfWriter(_pdfReader.Reader.Buffer, _nextFreeObjectNumber)

        If _documentInformation Is Nothing Then
            _documentInformation = New ClsPdfDocumentInformation
        End If

        'We always set the modification date attribute
        _documentInformation.SetModificationDate = Now
        _pdfWriter.Write(_documentInformation)

        If Not _viewPreferencesDictionnary Is Nothing Then
            If _viewPreferencesDictionnary.Dirty Then
                'We write the changes to the viewer preferences
                If _viewPreferencesDictionnary.ObjectNumber = 0 Then
                    objectNumber = _pdfWriter.AllocateObjectNumber
                    _viewPreferencesDictionnary.ObjectNumber = objectNumber
                End If
                _pdfWriter.Write(_viewPreferencesDictionnary)

                'Viewer preferences indirect object is stored in the catalog object
                obj = CreateIndirectReferenceObject(_viewPreferencesDictionnary.ObjectNumber, _viewPreferencesDictionnary.GenerationNumber)
                _catalogDictionnary.Add(PDF_VIEWERPREFERENCES, obj, True)
                _catalogDictionnary.Modified()
            End If
        End If

        For Each image In _images
            objectNumber = _pdfWriter.AllocateObjectNumber
            _pdfWriter.Write(image, objectNumber)
            image.Stream.StreamObject.ObjectNumber = objectNumber
            image.Stream.StreamObject.GenerationNumber = 0
        Next

        'We get the liste of all the pages modified and we write them
        pagesModified = _pageTree.GetPagesModified
        For Each page In pagesModified

            If page.AddedAnnotations.Count > 0 Then
                'We write annotations added to the page
                Dim annotation As ClsPdfAnnotation
                For Each annotation In page.AddedAnnotations
                    objectNumber = _pdfWriter.AllocateObjectNumber
                    _pdfWriter.Write(annotation, objectNumber)
                    page.AddAnnotationIndirectReference(objectNumber, 0)
                Next
                page.SaveAnnotations()
            End If

            If page.AddedImageNames.Count > 0 Then
                page.SaveImages()
                'We write streams added to the page
                Dim stream As clsPdfStreamObject
                For Each stream In page.AddedStreams
                    objectNumber = _pdfWriter.AllocateObjectNumber
                    _pdfWriter.Write(stream, objectNumber)
                    page.AddContentIndirectReference(objectNumber, 0)
                Next
                page.SaveContents()
            End If

            _pdfWriter.Write(page)
        Next

        If _catalogDictionnary.Dirty Then
            _pdfWriter.WriteDictionnary(_catalogDictionnary)
        End If

        If _pdfReader.HasCrossReferenceStream Then
            'we write a cross-reference stream
            _pdfWriter.WriteXRefStream(_pdfReader.LastXrefTablePosition, _xrefTable.Count, _trailerDictionnary, _documentInformation)
        Else
            'We write a standard cross-reference 
            _pdfWriter.WriteXRef()
            _pdfWriter.WriteTrailer(_lastXrefTable, _xrefTable.Count, _trailerDictionnary)
        End If
        _pdfWriter.Close()
        Close()
    End Sub
    ''' <summary>
    ''' Copy the contents of the pdf writer to the specified file
    ''' </summary>
    ''' <param name="filePath"></param>
    ''' <remarks></remarks>
    Private Sub _copyTo(ByVal filePath As String)
        'We copy the temp file over the original pdf file
        Dim f As New FileInfo(_pdfWriter.FileName)
        f.CopyTo(filePath, True)

        Try
            f.Delete()  'We delete the temp file
        Catch ex As Exception

        End Try

        _pdfWriter = Nothing
    End Sub
    ''' <summary>
    ''' Add new trailer values to the trailer dictionnary
    ''' </summary>
    ''' <param name="trailerDict"></param>
    ''' <remarks></remarks>
    Private Sub _addToTrailer(ByVal trailerDict As clsPdfDictionnary)
        Dim key As String
        Dim obj As clsPdfBaseObject
        For Each key In trailerDict.Hashtable.Keys
            obj = trailerDict.Item(key)
            _trailerDictionnary.Add(key, obj, False)
        Next
    End Sub
    ''' <summary>
    ''' Read the encryption dictionnary
    ''' </summary>
    ''' <remarks></remarks>
    Private Sub _readEncryptionDictionnary()
        If IsEncrypt Then
            Dim obj As clsPdfBaseObject
            obj = _trailerDictionnary.Item(PDF_ENCRYPT)
            If Not obj Is Nothing Then
                obj = GetPdfObjectFromIndirectReferenceObject(obj)
                obj.ParseChildObjects()
                If obj.ChildsCount = 1 Then
                    _encryptionDictionnary = New clsPdfEncryption(obj.child(0).GetPdfDictionnary.Hashtable)
                End If
            End If
        End If
    End Sub
    ''' <summary>
    ''' Read the PDF file information object
    ''' </summary>
    ''' <remarks></remarks>
    Private Sub _readDocumentInformation()
        Dim obj As clsPdfBaseObject
        _documentInformation = Nothing

        obj = _trailerDictionnary.Item(PDF_INFO)
        If Not obj Is Nothing Then
            obj = GetPdfObjectFromIndirectReferenceObject(obj)

            obj.ParseChildObjects()
            If obj.ChildsCount = 1 Then
                _documentInformation = New ClsPdfDocumentInformation(obj.child(0).GetPdfDictionnary.Hashtable)
                _documentInformation.ObjectNumber = obj.ObjectNumber
                _documentInformation.GenerationNumber = obj.GenerationNumber
            End If
        End If
        If _documentInformation Is Nothing Then
            'If there is no document information object (which is possible but not a best practice),
            'we create en empty document information object
            _documentInformation = New ClsPdfDocumentInformation()
        End If
    End Sub
    ''' <summary>
    ''' Read the viewer preferences
    ''' </summary>
    ''' <remarks></remarks>
    Private Sub _readViewerPreferences()
        Dim obj As clsPdfBaseObject
        Dim dict As clsPdfDictionnary

        _viewPreferencesDictionnary = Nothing
        obj = _catalogDictionnary.Item(PDF_VIEWERPREFERENCES)

        If Not obj Is Nothing Then
            dict = GetPdfDictionnaryFromDirectOrIndirectReference(obj, Me)

            _viewPreferencesDictionnary = New clsPDFViewerPreferences(dict.Hashtable, _catalogDictionnary)
            _viewPreferencesDictionnary.ObjectNumber = obj.ObjectNumber
            _viewPreferencesDictionnary.GenerationNumber = obj.GenerationNumber
        End If
    End Sub
    ''' <summary>
    ''' Determine if the pdf is linearized, which means that it is optimized for fast viewing
    ''' </summary>
    ''' <returns></returns>
    ''' <remarks>This private method must me called immediately after the private method IsFilePDF()</remarks>
    Private Function _isPDFLinearized() As Boolean
        Dim dict As clsPdfDictionnary
        Dim obj As clsPdfBaseObject

        _isPDFLinearized = False

        'We read the next object of the PDF file, normally we should be at the top of the PDF file
        'immediately after the PDF version string
        obj = _pdfReader.Reader.GetObject

        'The next object could be a comment object
        If obj.Type = PdfObjectTypes.CommentObject Then
            obj = _pdfReader.Reader.GetObject
        End If

        If obj.Type = PdfObjectTypes.IndirectObject Then
            obj.ParseChildObjects()
            If obj.ChildsCount > 0 Then
                If obj.child(0).Type = PdfObjectTypes.DictionaryObject Then
                    dict = obj.child(0).GetPdfDictionnary
                    If dict.ContainsKey(PDF_LINEARIZED) Then
                        _isPDFLinearized = True
                    End If
                End If
            End If
        End If
    End Function
    ''' <summary>
    ''' Read all the object numbers for pages representing leaf nodes, 
    ''' a leaf node is what encapsulate all the objects needed in a page.
    ''' </summary>
    ''' <param name="obj"></param>
    ''' <remarks>We only have to call this private method if we intend to manipulate pages and their contents.</remarks>
    Private Sub _readAllPageTree(ByVal obj As clsPdfBaseObject)
        Dim pageNumber As Integer
        Try
            pageNumber = 0
            _pageTree = New clsPDFPageTree
            _pageTree.Parent = Nothing  'This is the root tree
            _pageTree.ObjectNumber = obj.ObjectNumber
            _pageTree.GenerationNumber = obj.GenerationNumber
            _readPages(GetPdfObjectFromIndirectReferenceObject(obj), _pageTree, pageNumber)

            'The total page count is in the first page tree node (the root tree)
            '_pageCount = _pageTree.PageInformationDictionnary.Item(PDF_COUNT).GetLong
            _pageCount = 0
            _pageTree.GetPageCount(_pageCount)
        Catch ex As Exception
            Throw New Exception("Error reading pages definition")
        End Try
    End Sub
    ''' <summary>
    ''' Traverse page tree node recursively in order to get all the object numbers for pages representing leaf nodes 
    ''' </summary>
    ''' <param name="obj"></param>
    ''' <remarks></remarks>
    Private Sub _readPages(ByVal obj As clsPdfBaseObject, ByRef pageTree As clsPDFPageTree, ByRef pageNumber As Integer)
        Dim table As Hashtable = Nothing
        '       Dim arrayObj As clsPdfBaseObject
        Dim pagesList As List(Of clsPdfBaseObject)

        If obj.Type = PdfObjectTypes.IndirectObject Then
            obj.ParseChildObjects()
            If obj.ChildsCount = 1 Then
                table = obj.child(0).GetPdfDictionnary.Hashtable
            End If
        End If

        pageTree.PageInformationDictionnary = New clsPDFPage(table, Me)
        'arrayObj = pageTree.PageInformationDictionnary.Item(PDF_KIDS)
        pagesList = GetPdfArrayFromDirectOrIndirectReference(pageTree.PageInformationDictionnary.Item(PDF_KIDS), Me)
        'pagesList = arrayObj.GetArray
        If Not pagesList Is Nothing Then
            _readPages(pagesList, pageTree, pageNumber)
        End If

    End Sub
    Private Sub _readPages(ByVal pagesList As List(Of clsPdfBaseObject), ByRef pageTree As clsPDFPageTree, ByRef pageNumber As Integer)
        Dim obj, objPage, objType As clsPdfBaseObject
        Dim dict As clsPdfDictionnary
        Dim childPageTree As clsPDFPageTree

        For Each obj In pagesList
            If obj.Type = PdfObjectTypes.IndirectReferenceObject Then
                objPage = GetPdfObjectFromIndirectReferenceObject(obj)
                objPage.ParseChildObjects()
                dict = objPage.child(0).GetPdfDictionnary
                objType = dict.Item(PDF_TYPE)
                Select Case objType.GetString
                    Case PDF_PAGES
                        childPageTree = New clsPDFPageTree
                        childPageTree.ObjectNumber = obj.ObjectNumber
                        childPageTree.GenerationNumber = obj.GenerationNumber
                        pageTree.AddChild(childPageTree)
                        _readPages(objPage, childPageTree, pageNumber)
                    Case PDF_PAGE
                        'We have a leaf node, we keep its object number
                        childPageTree = New clsPDFPageTree
                        pageTree.AddChild(childPageTree)
                        childPageTree.PageInformationDictionnary = New clsPDFPage(dict.Hashtable, Me)
                        childPageTree.PageInformationDictionnary.ObjectNumber = obj.ObjectNumber
                        childPageTree.PageInformationDictionnary.GenerationNumber = obj.GenerationNumber
                        pageNumber += 1
                        childPageTree.PageNumber = pageNumber
                End Select
            End If
        Next
    End Sub
    ''' <summary>
    ''' Read the PDF catalog object
    ''' </summary>
    ''' <remarks></remarks>
    Private Sub _readCatalog()
        Dim obj, catalogObj As clsPdfBaseObject
        Try
            obj = _trailerDictionnary.Item(PDF_ROOT)
            catalogObj = GetPdfObjectFromIndirectReferenceObject(obj)
            catalogObj.ParseChildObjects()
            _catalogDictionnary = catalogObj.child(0).GetPdfDictionnary
            If _catalogDictionnary Is Nothing Then
                Throw New Exception("Error while reading the catalog object")
            Else
                _catalogDictionnary.ObjectNumber = catalogObj.ObjectNumber
                _catalogDictionnary.GenerationNumber = catalogObj.GenerationNumber
            End If
        Catch ex As Exception
            Throw New Exception("Error while reading the catalog object")
        End Try
    End Sub
    Private Function _getPdfObjectFromObjectNumber(ByVal objectNumber As Long) As clsPdfBaseObject
        Dim xref As ClsPdfXref
        _getPdfObjectFromObjectNumber = Nothing
        xref = _getPdfObjectXref(objectNumber)
        If Not xref Is Nothing Then
            _pdfReader.Reader.ReaderPosition = xref.Offset
            _getPdfObjectFromObjectNumber = _pdfReader.Reader.GetObject
        End If
    End Function
    ''' <summary>
    ''' Gives the absolute file position of an object in the PDF file
    ''' </summary>
    ''' <param name="obj"></param>
    ''' <returns></returns>
    ''' <remarks></remarks>
    Private Function _getPdfObjectPosition(ByVal obj As clsPdfBaseObject) As Long
        Dim xref As ClsPdfXref

        xref = _getPdfObjectXref(obj)
        _getPdfObjectPosition = xref.Offset
    End Function
    ''' <summary>
    ''' Get the entry of the cross-reference table corresponding to a pdf object
    ''' </summary>
    ''' <param name="obj"></param>
    ''' <returns></returns>
    ''' <remarks></remarks>
    Private Function _getPdfObjectXref(ByVal obj As clsPdfBaseObject) As ClsPdfXref
        Dim objectNumber As Long

        objectNumber = obj.GetLong
        _getPdfObjectXref = _getPdfObjectXref(objectNumber)
    End Function
    ''' <summary>
    ''' Get the entry of the cross-reference table corresponding to a pdf object number
    ''' </summary>
    ''' <param name="objectNumber"></param>
    ''' <returns></returns>
    ''' <remarks></remarks>
    Private Function _getPdfObjectXref(ByVal objectNumber As Long) As ClsPdfXref
        _getPdfObjectXref = Nothing
        If _xrefTable.ContainsKey(objectNumber) Then
            _getPdfObjectXref = TryCast(_xrefTable.Item(objectNumber), ClsPdfXref)
        End If
    End Function
    ''' <summary>
    ''' Determine if a file is a PDF file
    ''' </summary>
    ''' <returns></returns>
    ''' <remarks>We must be at the top of the file before calling this method
    ''' It is normally called just after opening the file.
    ''' </remarks>
    Private Function _isFilePDF() As Boolean
        Dim pdfObject As clsPdfBaseObject

        _isFilePDF = False
        pdfObject = _pdfReader.Reader.GetObject
        'The first object of a PDF file is a comment object containing a header
        'with the string "PDF-" followed by the version number of the PDF standard
        If pdfObject.Type = PdfObjectTypes.CommentObject Then
            _version = pdfObject.GetString()
            _isFilePDF = _version.StartsWith(PDF_TOKEN)
        End If
    End Function
    ''' <summary>
    ''' We get the next pdf free object number that will be attributed to objects added to the pdf file
    ''' </summary>
    ''' <returns></returns>
    ''' <remarks></remarks>
    Private Function _getNextFreeObject() As Long
        Dim highestObjNumber As Long
        Dim objNumber As Long

        _getNextFreeObject = -1
        highestObjNumber = -1
        For Each objNumber In _xrefTable.Keys
            If objNumber > highestObjNumber Then
                highestObjNumber = objNumber
            End If
        Next
        _getNextFreeObject = highestObjNumber + 1
    End Function
#End Region
End Class
